//! World provider that generates an infinite flat world of grass with a
//! structure near the origin.

use craftdig_core::registrar::BlockRegistry;
use craftdig_datamodel::block::*;
use craftdig_datamodel::world::ChunkState;
use craftdig_math::*;

use super::errors::*;
use super::traits::*;

#[derive(Debug)]
struct Palette {
    grass_id: BlkId,
    dirt_id: BlkId,
    stone_id: BlkId,
    bedrock_id: BlkId,

    /// The block used for the structure walls.
    structure_id: BlkId,
}

impl Palette {
    fn from_registry(reg: &BlockRegistry) -> Result<Self, Error> {
        let grass_id = reg
            .resolve_name("grass")
            .ok_or_else(|| Error::MissingBlockTy("grass".to_owned()))?;

        let dirt_id = reg
            .resolve_name("dirt")
            .ok_or_else(|| Error::MissingBlockTy("dirt".to_owned()))?;

        let stone_id = reg
            .resolve_name("stone")
            .ok_or_else(|| Error::MissingBlockTy("stone".to_owned()))?;

        let bedrock_id = reg
            .resolve_name("bedrock")
            .ok_or_else(|| Error::MissingBlockTy("bedrock".to_owned()))?;

        let smat = "wood_planks";

        let structure_id = reg
            .resolve_name(smat)
            .ok_or_else(|| Error::MissingBlockTy(smat.to_owned()))?;

        Ok(Palette {
            grass_id,
            dirt_id,
            stone_id,
            bedrock_id,
            structure_id,
        })
    }
}

#[derive(Debug)]
pub struct FlatgrassGen {
    palette: Palette,

    /// The y level that the grass generates at.
    y_base: i32,

    /// Number of dirt layers below the grass.
    dirt_layers: i32,

    /// Number of stone layers below the dirt before a layer of bedrock.
    stone_layers: i32,

    /// The height of the structure.
    structure_height: u32,

    /// The square radius of the structure.
    structure_sq_radius: u32,
}

impl FlatgrassGen {
    pub fn new(
        y_base: i32,
        dirt_layers: i32,
        stone_layers: i32,
        structure_height: u32,
        structure_sq_radius: u32,
        reg: &BlockRegistry,
    ) -> Result<Self, Error> {
        let palette = Palette::from_registry(reg)?;
        Ok(Self {
            palette,
            y_base,
            dirt_layers,
            stone_layers,
            structure_height,
            structure_sq_radius,
        })
    }

    fn compute_block_at(&self, pos: BlkCoord) -> Result<BlkId, Error> {
        // First, decide if we're even in the world properly.
        let depth = self.y_base - pos.y();
        let local_y = pos.y() - self.y_base;

        // Figure out the layers below the grass.
        let bedrock_depth = self.dirt_layers + self.stone_layers + 1;
        if depth == bedrock_depth {
            return Ok(self.palette.bedrock_id);
        }

        // Below bedrock we go back to air, which is the closest thing to
        // void we can do right now.
        if depth > bedrock_depth {
            return Ok(BlkId::zero());
        }

        // Then deciding what block to place in the interesting part of the
        // world depends on the bounds of the structure.
        let sqr = self.structure_sq_radius as i32;
        let isqr = self.structure_sq_radius as i32 - 1; // tier inset
        let dx = i32::abs(pos.x());
        let dz = i32::abs(pos.z());
        let manhattan = dx + dz;
        if dx <= sqr && dz <= sqr {
            if depth > 0 {
                return Ok(self.palette.stone_id);
            }

            if local_y > self.structure_height as i32 {
                return Ok(BlkId::zero());
            }

            // Compute a few guidelines around the structure.
            let ext_tier_height = (self.structure_height / 2) as i32;
            let tunnel_ceil_height = (self.structure_height * 3) / 4;
            let tunnel_hwidth = self.structure_sq_radius / 2; // half the width

            // Carve out corners.
            if local_y <= ext_tier_height && manhattan > (2 * sqr) - 4 {
                if local_y == 0 {
                    return Ok(self.palette.grass_id);
                }
                return Ok(BlkId::zero());
            }

            if local_y > ext_tier_height && manhattan > (2 * sqr) - 6 {
                if local_y == 0 {
                    return Ok(self.palette.grass_id);
                }
                return Ok(BlkId::zero());
            }

            // Dig the tunnel, east-west.
            if local_y < tunnel_ceil_height as i32
                && pos.z() > -(tunnel_hwidth as i32)
                && pos.z() < tunnel_hwidth as i32
            {
                if depth == 0 {
                    return Ok(self.palette.grass_id);
                }

                return Ok(BlkId::zero());
            }

            // The top section in set in slightly from the edge.
            if local_y > ext_tier_height && (dx > isqr || dz > isqr) {
                return Ok(BlkId::zero());
            }

            // The top and the platform of the tier are both a smooth texture,
            // so use stone.
            if local_y == ext_tier_height || local_y == self.structure_height as i32 {
                return Ok(self.palette.stone_id);
            }

            // Otherwise, fill with the standard building material.
            Ok(self.palette.structure_id)
        } else {
            // Check for above ground, return air.
            if depth < 0 {
                return Ok(BlkId::zero());
            }

            // Check for surface, do grass.
            if depth == 0 {
                return Ok(self.palette.grass_id);
            }

            // And then just the dirt/stone layering.
            if depth > self.dirt_layers + 1 {
                Ok(self.palette.stone_id)
            } else {
                Ok(self.palette.dirt_id)
            }
        }
    }
}

impl ChunkProvider for FlatgrassGen {
    fn provide_chunk(&self, coord: ChunkCoord) -> Result<ChunkState, Error> {
        let mut buf = ChunkState::new_full(BlkId::zero());

        for scp in SubchunkPos::iter() {
            let bpos = BlkCoord::from_parts(coord, scp);
            let id = self.compute_block_at(bpos)?;
            let bss = BlockStateSnapshot::new_simple(id);
            buf.set_block_state(scp, &bss);
        }

        Ok(buf)
    }
}
